跳到主要内容

数据模型

Data Model

数据模型其实是对 Python 框架的描述,它规范了这门语言自身构建模块的接口,这些模块包括但不限于序列、迭代器、函数、类和上下文管理器

通过实现特殊方法,自定义数据类型可以表现得跟内置类型一样,从而让我们写出更具表达力的代码——或者说,更具 Python 风格的代码。

特殊方法

Python 解释器碰到特殊的句法时,会使用特殊方法去激活一些基本的对象操作,这些特殊方法的名字以两个下划线开头,以两个下划线结尾(例如 __getitem__)。

比如 obj[key] 的背后就是 __getitem__ 方法,为了能求得 my_collection[key] 的值,解释器实际上会调用 my_collection.__getitem__(key)

魔术方法(magic method)是特殊方法的昵称,也叫双下方法(dunder method)

通过实现特殊方法来利用 Python 数据模型有两个好处:

  • 作为你的类的用户,他们不必去记住标准操作的各式名称(“怎么得到元素的总数?是 .size() 还是 .length() 还是别的什么?”)
  • 可以更加方便地利用 Python 的标准库,比如 random.choice 函数,从而不用重新发明轮子。

首先明确一点,特殊方法的存在是为了被 Python 解释器调用的,你自己并不需要调用它们。也就是说没有 my_object.__len__() 这种写法,而应该使用 len(my_object)。在执行 len(my_object) 的时候,如果 my_object 是一个自定义类的对象,那么 Python 会自己去调用其中由你实现的 __len__ 方法。

如果是 Python 内置的类型,比如列表(list)、字符串(str)、字节序列(bytearray)等,那么 CPython 会抄个近路,__len__ 实际上会直接返回 PyVarObject 里的 ob_size 属性PyVarObject 是表示内存中长度可变的内置对象的 C 语言结构体。直接读取这个值比调用一个方法要快很多。

很多时候,特殊方法的调用是隐式的,比如 for i in x: 这个语句,背后其实用的是 iter(x),而这个函数的背后则是 x.__iter__() 方法(当然前提是这个方法在 x 中被实现了)

repr

Python 有一个内置的函数叫 repr,它能把一个对象用字符串的形式表达出来以便辨认,这就是 “字符串表示形式”。

repr 就是通过 __repr__ 这个特殊方法来得到一个对象的字符串表示形式的。如果没有实现 __repr__,当我们在控制台里打印一个向量的实例时,得到的字符串可能会是 <Vector object at 0x10e100070>

__repr____str__ 的区别在于,后者是在 str() 函数被使用,或是在用 print 函数打印一个对象的时候才被调用的,并且它返回的字符串对终端用户更友好。

有时,__repr__ 方法返回的字符串足够友好,无须再定义 __str__ 方法,因为继承自 object 类的实现最终会调用 __repr__ 方法

算术运算符

__add____mul__ 这两个方法的返回值都是新创建的对象,被操作的两个对象(selfother)还是原封不动,代码里只是读取了它们的值而已。

def __add__(self, other):
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)

中缀运算符的基本原则就是不改变操作对象,而是产出一个新的值

布尔值

尽管 Python 里有 bool 类型,但实际上任何对象都可以用于需要布尔值的上下文中(比如 ifwhile 语句,或者 andornot 运算符)。为了判定一个值 x还是为,Python 会调用 bool(x),这个函数只能返回 True 或者 False

默认情况下,我们自己定义的类的实例总被认为是真的,除非这个类对 __bool__ 或者 __len__ 函数有自己的实现。bool(x) 的背后是调用 x.__bool__() 的结果;如果不存在 __bool__ 方法,那么 bool(x) 会尝试调用 x.__len__()。若返回 0,则 bool 会返回 False;否则返回 True

容器 API

图中所有的类都是抽象基类

以斜体显示的方法名称表示抽象方法,必须由具体子类(例如 list 和 dict)实现。其他方法有具体实现,子类可以直接继承

image.png|580

顶部 3 个抽象基类均只有一个特殊方法。抽象基类 Collection(Python 3.6 新增)统一了这 3 个基本接口,每一个容器类型均应实现如下事项:

  • Iterable 要支持 for、拆包和其他迭代方式;
  • Sized 要支持内置函数 len;
  • Container 要支持 in 运算符。

Python 不强制要求具体类要显示继承这些抽象基类。比如,只要实现了 len 方法,就说明那个类满足 Sized 接口。

Collection 有 3 个十分重要的专用接口:

  • Sequence 规范 list 和 str 等内置类型的接口;
  • Mapping 被 dict、collections.defaultdict 等实现;
  • Set 是 set 和 frozenset 两个内置类型的接口。

只有 Sequence 实现了 Reversible,因为序列要支持以任意顺序排列内容,而 Mapping 和 Set 不需要。

自 Python 3.7 开始,dict 类型正式“有顺序”了,不过只是保留键在插入时的顺序。你不能随意重新排列 dict 中的键。

Set 抽象基类中的所有特殊方法实现的都是中缀运算符。例如,a & b 计算集合 a 和 b 的交集,该运算符由 and 特殊方法实现。

特殊方法一览

Python 语言参考手册中的“Data Model”(https://docs.python.org/3/reference/datamodel.html)一章列出了 83 个特殊方法的名字,其中 47 个用于实现算术运算、位运算和比较操作。

跟运算符无关的特殊方法

类别方法名
字符串 / 字节序列表示形式__repr____str____format____bytes__
数值转换__abs____bool____complex____int____float____hash____index__
集合模拟__len____getitem____setitem____delitem____contains__
迭代枚举__iter____reversed____next__
可调用模拟__call__
上下文管理__enter____exit__
实例创建和销毁__new____init____del__
属性管理__getattr____getattribute____setattr____delattr____dir__
属性描述符__get____set____delete__
跟类相关的服务__prepare____instancecheck____subclasscheck__

跟运算符相关的特殊方法

类别方法名和对应的运算符
一元运算符__neg__ -__pos__ +__abs__ abs()
众多比较运算符__lt__ <__le__ <=__eq__ ==__ne__ !=__gt__ >__ge__ >=
算术运算符__add__ +__sub__ -__mul__ *__truediv__ /__floordiv__ //__mod__ %__divmod__ divmod()__pow__ **pow()__round__ round()
反向算术运算符__radd____rsub____rmul____rtruediv____rfloordiv____rmod____rdivmod____rpow__
增量赋值算术运算符__iadd____isub____imul____itruediv____ifloordiv____imod____ipow__
位运算符__invert__ ~__lshift__ <<__rshift__ >>__and__ &__or__、`xor ^`
反向位运算符__rlshift____rrshift____rand____rxor____ror__
增量赋值位运算符__ilshift____irshift____iand____ixor____ior__

其他

  • 对序列数据类型的模拟是特殊方法用得最多的地方
  • 迭代通常是隐式的,譬如说一个集合类型没有实现 __contains__ 方法,那么 in 运算符就会按顺序做一次迭代搜索。